package org.linuxgears.BouncingBall.model;

import java.awt.*;
import java.util.*;

import org.linuxgears.BouncingBall.model.context.BallContext;
import org.linuxgears.BouncingBall.model.context.BallRunnable;
import org.linuxgears.BouncingBall.model.context.Border;
import org.linuxgears.BouncingBall.model.context.BorderBounceMutator;
import org.linuxgears.BouncingBall.model.context.BorderBounceTrigger;
import org.linuxgears.BouncingBall.model.context.ConditionalContextMutator;
import org.linuxgears.BouncingBall.mover.SimpleMover;
import org.linuxgears.BouncingBall.mover.strategy.MovingStrategy;
import org.linuxgears.BouncingBall.mover.strategy.LineMovingStrategy;
import org.linuxgears.BouncingBall.mover.strategy.PausableMovingStrategy;
import org.linuxgears.BouncingBall.painter.BallPainterFactory;
import org.linuxgears.BouncingBall.painter.SimpleBallPainter;
import org.linuxgears.BouncingBall.tools.BallTools;

public class BouncingBallModel {
    public static int WIDTH = 650;
    public static int HEIGHT = 400;
    public static Point DEFAULT_POSITION = new Point(0, 0);
    private Rectangle bounds = new Rectangle(0, 0, WIDTH, HEIGHT);
	
	private HashMap<Ball, BallContext> contextMap;
    private Ball activeBall = null;

    private boolean paused;
    
    
    private Memento savedState;
    private Memento initialState;

    public BouncingBallModel() {
        contextMap = new HashMap<Ball, BallContext>();
        initialState = getMemento();
        paused = false;
    }
    
    public Rectangle getBounds() {
		return bounds;
	}
    
    public BallRunnable addBall(Ball ball) {
        return addBall(ball, (Point)DEFAULT_POSITION.clone());
    }

    public BallRunnable addBall(Ball ball, Point position) {
    	return addBall(ball, position, null);
    }
    
    public BallRunnable addBall(Ball ball, Point position, MovingStrategy movingStrategy) {
    	MovingStrategy ms = movingStrategy;
    	if (null == ms) {
    		ms = new PausableMovingStrategy(new LineMovingStrategy());
    		//ms = new LineMovingStrategy();
    		ms.setTargetPoint(new Point(2, 5));
    	};

    	BallContext ballContext = new BallContext(ball, bounds, position, new SimpleMover<Ball>(ms));
        contextMap.put(ball, ballContext);
        
        ConditionalContextMutator<BallContext> cbcm = new ConditionalContextMutator<BallContext>();
        
        for (Border b: Border.values()) {
        	cbcm
        		.on(new BorderBounceTrigger(b))
        		.add(new BorderBounceMutator(b));
        };
        
        ballContext.setMutator(cbcm);
        
        return ballContext.getBallRunnable();
    }

    public void removeBall(Ball ball) {
    	if (ball == null) return;
		
    	contextMap.get(ball).getBallRunnable().interrupt();
		contextMap.remove(ball);

		if (activeBall == ball) {
			activeBall = null;
		}
    }
    
    public BallContext getBallContext(Ball ball) {
        return contextMap.get(ball);
    }

    public Collection<SimpleBallPainter> getPainters() {
        LinkedList<SimpleBallPainter> painters = new LinkedList<SimpleBallPainter>();

        BallPainterFactory bpf = BallPainterFactory.getInstance();
        for (Ball ball: contextMap.keySet()) {
            if ( isSelected(ball)) {
                painters.add(bpf.createSelectedBallPainter(ball, contextMap.get(ball)));
            } else {
                painters.add(bpf.createBallPainter(ball, contextMap.get(ball)));
            }
        }

        return painters;
    }

    // Selection methods
    public void selectBall(Ball ball) {
    	if (contextMap.keySet().contains(ball)) {
    		this.activeBall = ball;
    	}
    }

    public boolean isSelected(Ball ball) {
    	return activeBall == ball;
    }

    public void selectNextBall() {
        Iterator<Ball> ibr = contextMap.keySet().iterator();
        if (! ibr.hasNext()) return;		// no balls
        
        Ball first = ibr.next();
        if (activeBall == null) {
            activeBall = first;
        } else if (isSelected(first) && ibr.hasNext()) {
            activeBall = ibr.next();
        } else {
            while (ibr.hasNext() && (! isSelected(ibr.next())) ) {
                // skip this runnable
            };

            if (ibr.hasNext()) {
                activeBall = ibr.next();
            } else {
                activeBall = first;
            }
        }
    }
    
    public void selectBallAt(Point position) {
    	for (Ball ball: contextMap.keySet()) {
    		if (pointBelongsTo(position, ball)) {
    			activeBall = ball;
    			break;
    		}
    	}
    }
	
    public boolean pointBelongsTo(Point position, Ball ball) {
    	BallContext xtc = contextMap.get(ball);
    	return (4 * BallTools.distanceSq(position, BallTools.getBallCenterByPosition(ball, xtc.getPosition())) < BallFactory.DEFAULT_BALL_SIZE * BallFactory.DEFAULT_BALL_SIZE );
    }

	public void selectPreviousBall() {
		Iterator<Ball> ibr = contextMap.keySet().iterator();
		if (! ibr.hasNext()) return;		// no balls!
		
		Ball first = ibr.next();
		
		if (null == activeBall || first == activeBall) {
			// select last ball
			activeBall = first;		// may be or may be not, but now it is for sure
			while (ibr.hasNext()) {
				activeBall = ibr.next();
			}
		} else {
			Ball activeCandidate = first;
			
			while (ibr.hasNext()) {
				Ball next = ibr.next();
				
				if (next == activeBall) {
					activeBall = activeCandidate;
					break;
				} else {
					activeCandidate = next;
				}
			}
		}
	}

    // Model state control
    public void toggle() {
    	for (BallContext context: contextMap.values()) {
    		if (paused) {
    			((PausableMovingStrategy)context.getMover().getMovingStrategy()).resume();
    		} else {
    			((PausableMovingStrategy)context.getMover().getMovingStrategy()).pause();
    		}
    	}
    	paused = !paused;
    }

    // Save/restore state functionality
    
    @SuppressWarnings("unchecked")
	public Memento getMemento() {
    	Memento m = new Memento();
    	m.bounds = (Rectangle)bounds.clone();
    	m.activeBall = activeBall;
    	m.contextMap = (HashMap<Ball, BallContext>)contextMap.clone();
    	return m;
    }
    
    public void setMemento(Memento m) {
    	this.bounds = m.bounds;
    	this.activeBall = m.activeBall;
    	
    	// now we want to kill any BallRunnables that are not in saved context
    	
//    	this.contextMap.keySet().retainAll(m.contextMap.keySet());
    	
    	Set<Ball> assesToRemove = new HashSet<Ball>(this.contextMap.keySet());
    	assesToRemove.removeAll(m.contextMap.keySet());
    	
    	for (Ball ass: assesToRemove) {
    		contextMap.get(ass).getBallRunnable().interrupt();
    	}
    	
    	this.contextMap.keySet().retainAll(m.contextMap.keySet());
    	 	
    	// TODO: add new balls from memento state  
    	// generally we would also want to recreate any new BallRunnables from memento 
    	// not present in this.contextMap
    }
    
    public void saveState() {
    	savedState = getMemento();
    }
    
    public void restoreState() {
    	setMemento(savedState);
    }
    
    public void resetState() {
    	setMemento(initialState);
    }
    
    // pattern: Memento
    public static class Memento {
    	private Rectangle bounds;
    	private Ball activeBall;
    	private HashMap<Ball, BallContext> contextMap;
    }

	public void deleteBallRunnable(BallRunnable selected) {
		// TODO Auto-generated method stub
		
	}

	public void moveSelectedBallTo(Point newPosition) {
		if (activeBall == null) return;
		
		MovingStrategy ms = getBallContext(activeBall).getMover().getMovingStrategy();
		ms.setPosition(getBallContext(activeBall).getPosition());
		ms.setTargetPoint(BallTools.getBallPositionByCenter(activeBall, newPosition));
	}


	public Ball getActiveBall() {
		return activeBall;
	}

}
